77장. Saga 패턴 맛보기 — 분산 트랜잭션의 현실
이 장에서 말하고자 하는 것
서비스가 하나라면 한 트랜잭션으로 묶을 수 있다.
BEGIN;
INSERT INTO orders ...;
UPDATE inventory SET stock = stock - 1;
INSERT INTO payments ...;
COMMIT;
마이크로서비스에서는 이게 불가능하다.
orders 서비스 (DB1)
inventory 서비스 (DB2)
payments 서비스 (DB3)
세 DB에 걸친 트랜잭션을 한 번에 묶는 분산 트랜잭션은
이론적으로 가능하지만 실전에서는 거의 쓰지 않는다 (느림 · 복잡 · 한 곳 죽으면 전체 멈춤).
대신 쓰는 패턴이
Saga
다.
1. Saga의 아이디어
큰 트랜잭션을 작은 단계로 쪼개고
실패하면 보상(Compensation) 트랜잭션으로 되돌린다
Step 1: 주문 생성 (orders)
Step 2: 재고 차감 (inventory)
Step 3: 결제 (payments)
만약 Step 3에서 실패하면
Compensation 3: (결제는 못 했지만 추가 작업 없음)
Compensation 2: 재고 복원
Compensation 1: 주문 취소 처리
각 서비스는 자기 DB의 로컬 트랜잭션만 다룬다.
“최종 일관성(Eventual Consistency)” 을 받아들이는 게 핵심이다
2. 두 가지 Saga 스타일
Choreography — 안무
서비스끼리 이벤트를 주고받으며 스스로 다음 단계를 결정.
orders: 주문 생성 → "OrderCreated" 발행
↓
inventory: 받음 → 재고 차감 → "InventoryReserved" 발행
↓
payments: 받음 → 결제 → "PaymentSucceeded" 발행
↓
orders: 받음 → 주문 확정
- 중앙 컨트롤러 없음
- 단순한 흐름에 자연스러움
- 흐름이 복잡해지면 추적이 어려움
Orchestration — 지휘자
별도의 컨트롤러가 각 단계를 호출.
[Saga Orchestrator (Step Functions 등)]
├─ orders.createOrder()
├─ inventory.reserveStock()
├─ payments.charge()
└─ orders.confirmOrder()
실패 시 보상:
├─ payments.refund()
└─ inventory.releaseStock()
└─ orders.cancelOrder()
- 흐름이 한 곳에 있어 가독성 좋음
- 복잡한 흐름에 유리
- Step Functions 같은 도구가 잘 어울림
3. AWS 도구 매핑
| Saga 방식 | AWS 조합 |
|---|---|
| Choreography | SNS / EventBridge + SQS + 각 서비스 |
| Orchestration | Step Functions + Lambda / ECS |
4. 멱등성과 보상의 현실
Saga의 핵심 어려움은
보상이 항상 깨끗하지 않다
는 점이다.
"결제 환불" 은 항상 가능한가?
"발송된 알림" 은 어떻게 되돌리나?
"외부에 보낸 신호" 는?
- 모든 단계는 멱등하게 설계 (한 번 받아도 두 번 받아도 같은 결과)
- “이미 처리됐는지” 를 알 수 있는 식별자 (saga_id, transaction_id) 가 필요
- 비즈니스적으로 “되돌릴 수 없는” 행동은 가능한 한 뒤로 미룬다
5. Step Functions — Orchestration 도구
AWS의 워크플로우 엔진.
{
"StartAt": "CreateOrder",
"States": {
"CreateOrder": {
"Type": "Task",
"Resource": "arn:aws:lambda:...:createOrder",
"Next": "ReserveStock",
"Catch": [{ "ErrorEquals": ["States.ALL"], "Next": "Fail" }]
},
"ReserveStock": { ... },
"Charge": { ... },
"ConfirmOrder": { "End": true },
"Fail": { "Type": "Fail" }
}
}
- JSON으로 흐름 정의
- 각 단계의 재시도 / 타임아웃 / 보상 흐름을 선언적으로 표현
- 시각화 · 추적이 강함
복잡한 Saga는 Step Functions가 거의 정답
6. Outbox Pattern — 이벤트 발행의 함정
Saga의 흔한 함정.
서비스가 DB에 쓴 직후 곧장 이벤트 발행
↓
DB는 커밋됐는데 이벤트 발행이 실패 (또는 그 반대)
↓ 일관성 깨짐
해결:
1. DB에 "이벤트 outbox" 테이블도 같이 INSERT (같은 트랜잭션)
2. 별도 프로세스가 outbox를 읽어 이벤트로 발행
3. 발행 후 outbox에 표시
이걸 Transactional Outbox 패턴이라 한다.
DynamoDB Streams · Debezium 등으로 풀기도 한다.
7. 우리 서비스에서
주문 흐름 (Choreography):
[orders] 주문 생성 → EventBridge "OrderCreated"
↓ SQS
[inventory] 재고 차감 → "InventoryReserved"
↓ SQS
[payments] 결제 → "PaymentSucceeded"
↓ SQS
[orders] 주문 확정 → "OrderConfirmed"
실패 시 보상:
[payments] 실패 → "PaymentFailed"
↓ SQS
[inventory] 재고 복원
[orders] 주문 취소
복잡한 흐름이면 Step Functions로 옮긴다.
8. 직접 확인해보기 — Step Functions
aws stepfunctions create-state-machine \
--name OrderSaga \
--definition file://order-saga.json \
--role-arn <role-arn>
aws stepfunctions start-execution \
--state-machine-arn <sm-arn> \
--input '{"orderId":"o-1","userId":"u-1","amount":15000}'
콘솔에서 시각화된 실행 흐름을 볼 수 있다 — 디버깅에 강력하다.
9. 코드로는 이렇게 생겼다 — Terraform (Step Functions)
resource "aws_sfn_state_machine" "order_saga" {
name = "OrderSaga"
role_arn = aws_iam_role.sfn.arn
definition = jsonencode({
StartAt = "CreateOrder"
States = {
CreateOrder = {
Type = "Task"
Resource = "arn:aws:states:::lambda:invoke"
Parameters = {
FunctionName = aws_lambda_function.create_order.arn
"Payload.$" = "$"
}
Next = "ReserveStock"
Retry = [{
ErrorEquals = ["States.ALL"]
IntervalSeconds = 2
MaxAttempts = 3
BackoffRate = 2
}]
Catch = [{
ErrorEquals = ["States.ALL"]
Next = "Fail"
}]
}
ReserveStock = { ... }
Charge = { ... }
ConfirmOrder = { Type = "Succeed" }
Fail = { Type = "Fail" }
}
})
}
10. 이렇게 쓰면 망한다 — 안티패턴
안티패턴 1. 분산 트랜잭션을 강요한다
2PC(Two-Phase Commit) 류는 마이크로서비스에 거의 안 맞는다.
안티패턴 2. 보상이 어려운 단계를 앞에 둔다
환불 불가능한 결제를 먼저 하면 보상이 불가능.
되돌릴 수 있는 작업을 앞에, 되돌릴 수 없는 작업을 가장 뒤에
안티패턴 3. 멱등성이 없다
재시도가 두 번째 처리에 또 결제하는 사고를 만든다.
안티패턴 4. Outbox 없이 “DB 쓰고 이벤트 발행”
실패 시 일관성이 깨진다.
Transactional Outbox 또는 CDC(DB Streams)로 풀자
11. 한 줄로 정리
Saga는 분산 트랜잭션의 현실적 대안이며,
“되돌릴 수 있는 단계 + 멱등성 + 명시적 보상” 이 핵심이다
12. 이 장의 핵심 정리
- 마이크로서비스에서는 ACID 분산 트랜잭션 대신 Saga를 쓴다.
- Choreography(이벤트 안무) 와 Orchestration(컨트롤러) 두 스타일이 있다.
- 단순 흐름은 Choreography, 복잡한 흐름은 Step Functions로 Orchestration.
- 모든 단계는 멱등해야 한다.
- 되돌릴 수 없는 작업은 가장 뒤로 미룬다.
- Transactional Outbox로 DB-이벤트 일관성을 지킨다.